package jwt

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha512"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"os"
	"strconv"
	"time"

	"github.com/dgrijalva/jwt-go"
	"golang.org/x/crypto/pbkdf2"
)

const (
	TokenExpired = "token expired"
)

var (
	UnexpectedSingingMethod = errors.New("unexpected signing method")
	UnknownEntity           = errors.New("unknown entity")

	TokenLifeInHour   = parseTokenLife()
	TokenLifeInSecond = int(time.Hour * TokenLifeInHour / time.Second)
	Issuer            = parseIssuer()
)

// FIXME: 调整token 过期时间为一个月
func parseTokenLife() time.Duration {
	if hours := os.Getenv("TOKEN_LIFT"); hours != "" {
		a, _ := strconv.Atoi(hours)
		return time.Duration(a)
	}
	return time.Duration(24)
}

// TODO: unify LoginInfo struct with API
type LoginInfo struct {
	UserName string `json:"user_name"`
	Password string `json:"password"`
	UserID   string `json:"user_id"`
	Role     string `json:"-"`
	Phone    string `json:"phone"`
	Email    string
}

type Token struct {
	// jwt Token
	Token string `json:"token"`
	// Expires time, unit: second
	ExpiresIn int `json:"expiresIn"`
	// issue time
	IssuedAt time.Time `json:"issuedAt"`
	UserID   string    `json:"userID"`

	IsFirstLogin bool `json:"isFirstLogin,omitempty"`
}

func (l *LoginInfo) GetUserName() string {
	return l.UserName
}

func (l *LoginInfo) DecodeFromBase64() error {
	//userName, err := base64.StdEncoding.DecodeString(l.UserName)
	//if err != nil {
	//	return err
	//}

	passWord, err := base64.StdEncoding.DecodeString(l.Password)
	if err != nil {
		return err
	}

	//l.UserName = string(userName)
	l.Password = string(passWord)
	return nil
}

func (t Token) String() string {
	c, e := json.Marshal(t)
	if e != nil {
		panic(e)
	}
	return string(c)
}

func parseIssuer() string {
	return "cloud_android_backend"
}

type entity struct {
	Encrypted string `json:"encrypted"`
	jwt.StandardClaims
}

func Authorize(loginInfo *LoginInfo) (dto *Token, err error) {
	now := time.Now()
	var encrypted string
	if encrypted, err = loginInfo.Encrypt(); err != nil {
		return
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS512, &entity{
		Encrypted: encrypted,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: now.Add(time.Hour * TokenLifeInHour).Unix(),
			Issuer:    Issuer,
		},
	})
	var rawToken string
	if rawToken, err = token.SignedString(secret); err != nil {
		return
	}
	dto = &Token{
		Token:     rawToken,
		ExpiresIn: TokenLifeInSecond,
		IssuedAt:  now,
		UserID:    loginInfo.UserID,
	}
	return
}

func Validate(rawToken string) (loginInfo *LoginInfo, err error) {
	var token *jwt.Token
	if token, err = jwt.ParseWithClaims(rawToken, &entity{}, validateTenxSecret); err != nil {
		return
	}
	obj, ok := token.Claims.(*entity)
	if !ok {
		err = UnknownEntity
	}
	loginInfo, err = Decrypt(obj.Encrypted)
	return
}

func IsTokenExpiredErr(err error) bool {
	if jve, ok := err.(*jwt.ValidationError); ok && jve.Inner != nil {
		return jve.Errors&jwt.ValidationErrorExpired == jwt.ValidationErrorExpired
	}
	return false
}

func validateTenxSecret(token *jwt.Token) (obj interface{}, err error) {
	if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		err = UnexpectedSingingMethod
		return
	}
	return secret, nil
}

func (i LoginInfo) Encrypt() (encrypted string, err error) {
	var buffer []byte
	if buffer, err = json.Marshal(i); err != nil {
		return
	}
	if buffer, err = AEADEncrypt(buffer); err != nil {
		return
	}
	encrypted = base64.StdEncoding.EncodeToString(buffer)
	return
}

func Decrypt(encrypted string) (decrypted *LoginInfo, err error) {
	var buffer []byte
	buffer, err = base64.StdEncoding.DecodeString(encrypted)
	if err != nil {
		return
	}
	if buffer, err = AEADDecrypt(buffer); err != nil {
		return
	}
	var t LoginInfo
	if err = json.Unmarshal(buffer, &t); err != nil {
		return
	}
	decrypted = &t
	return
}

var AEADsecret = []byte("dazyunsecretkeysforuserstenx20141019generatedKey")
var Undecryptable = errors.New("undecryptable input")

func key(salt []byte) []byte {
	return pbkdf2.Key(AEADsecret, salt, 2145, 32, sha512.New)
}

func randBytes(length int) (bytes []byte, err error) {
	bytes = make([]byte, length)
	if _, err = io.ReadFull(rand.Reader, bytes); err != nil {
		return
	}
	return
}

func AEADEncrypt(content []byte) (encrypted []byte, err error) {
	var salt []byte
	if salt, err = randBytes(64); err != nil {
		return
	}
	var block cipher.Block
	if block, err = aes.NewCipher(key(salt)); err != nil {
		return
	}
	var iv []byte
	if iv, err = randBytes(12); err != nil {
		return
	}
	if _, err = io.ReadFull(rand.Reader, iv); err != nil {
		return
	}
	var aes256gcm cipher.AEAD
	if aes256gcm, err = cipher.NewGCM(block); err != nil {
		return
	}
	withTag := aes256gcm.Seal(nil, iv, content, nil)
	encryptedLen := len(withTag) - 16
	withoutTag := withTag[:encryptedLen]
	tag := withTag[encryptedLen:]
	encrypted = bytes.Join([][]byte{salt, iv, tag, withoutTag}, []byte{})
	return
}

func AEADDecrypt(encrypted []byte) (content []byte, err error) {
	if len(encrypted) <= 92 {
		err = Undecryptable
		return
	}
	salt := encrypted[:64]
	iv := encrypted[64:76]
	tag := encrypted[76:92]
	withoutTag := encrypted[92:]
	withTag := bytes.Join([][]byte{withoutTag, tag}, []byte{})
	var block cipher.Block
	if block, err = aes.NewCipher(key(salt)); err != nil {
		return
	}
	var aes256gcm cipher.AEAD
	if aes256gcm, err = cipher.NewGCM(block); err != nil {
		return
	}
	content, err = aes256gcm.Open(nil, iv, withTag, nil)
	return
}
